package org.flexthinker.binding.value.support
{
    import mx.collections.IList;
    import mx.events.CollectionEvent;


    /**
     * <code>ValueModel</code> for objects of type <code>IList</code>.
     * This class adds a mechanism to clone the <code>IList</code> so that
     * the clone can be used to detect changes.
     *
     * The detector used to find changes is <code>ListValueChangeDetector</code>
     *
     * @see BufferedValueModel
     * @see ListValueChangeDetector
     */
    public class BufferedCollectionValueModel extends BufferedValueModel
    {
        protected var bufferedList:IList;

        private var wrappedType:Class;

        /**
         * Constructs a new <code>BufferedCollectionValueModel</code>
         *
         * @param valueModel
         *          The <code>ValueModel</code> being wrapped
         * @param wrappedType
         *          The class of the value contained by wrappedModel.
         *          This must be of type <code>IList</code>
         */
        public function BufferedCollectionValueModel(valueModel:AbstractValueModel, wrappedType:Class):void
        {
            super(valueModel);
            this.wrappedType = wrappedType;

            valueModel.setValueChangeDetector(new ListValueChangeDetector());
            setValueChangeDetector(new ListValueChangeDetector());
            init();
        }

        private function init():void
        {
            if (getWrappedValueModel() == null)
            {
                throw new Error("Please provide a non-null value model");
            }
            if (wrappedType == null)
            {
                throw new Error("Please provide a non-null wrapped type");
            }
            if (!(new wrappedType() is IList ))
            {
                throw new Error("WrappedType has to be an IList types.");
            }
            if (getWrappedValueModel().getValue() != null && !(getWrappedValueModel().getValue() is IList))
            {
                throw new Error("Wrapped ValueModel should work with IList types.");
            }

            updateBufferedList(getWrappedValueModel().getValue());
        }

        override public function getValue():Object
        {
            return bufferedList;
        }

        public function getBufferedList():IList
        {
            return bufferedList;
        }


        override public function setValue(value:Object):void
        {
            if (!this.equals(value))
            {
                var oldValue:Object = getValue();
                updateBufferedList(value);
                updateBuffering();
                fireValueChange(oldValue, bufferedList);
            }
        }

        override public function equals(obj:Object):Boolean
        {
            return !getValueChangeDetector().hasValueChanged(getBufferedList(), obj);
        }


        override public function commit():void
        {
            super.commit();
            updateBuffering();
        }

        override protected function getValueToCommit():Object
        {
            return createCollectionFrom(bufferedList.toArray());
        }

        /**
         * this is called on setValue and on creation.
         * it might be expensive to do so on large collections.
         * Needs further investigation to search for optimizations.
         * @param value
         *          the new value
         */
        private function updateBufferedList(value:Object):void
        {
            var bufferedArrayValue:Array = value != null ? IList(value).toArray() : [];
            if (bufferedList)
            {
                bufferedList.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
                        bufferedList_collectionChangeHandler)
            }

            bufferedList = createCollectionFrom(bufferedArrayValue);

            bufferedList.addEventListener(CollectionEvent.COLLECTION_CHANGE, bufferedList_collectionChangeHandler,
                    false, 0, true);
        }

        private function createCollectionFrom(bufferedArrayValue:Array):IList
        {
            var retVal:IList;

            try
            {
                // ArrayCollection and ArrayList accept an array in the constructor
                retVal = new wrappedType(bufferedArrayValue) as IList;
            }
            catch (err:Error)
            {
                // other list implementations are custom so we'll clone elements manually
                retVal = new wrappedType() as IList;
                for (var i:int = 0; i < bufferedArrayValue.length; i++)
                {
                    retVal.addItem(bufferedArrayValue[i]);
                }
            }

            return retVal;
        }

        private function updateBuffering():void
        {
            var wasBuffering:Boolean = isBuffering();
            buffering = !getWrappedValueModel().equals(getBufferedList());
        }

        private function bufferedList_collectionChangeHandler(event:CollectionEvent):void
        {
            updateBuffering();
            //TODO: fireValueChange differently
            fireValueChange(bufferedList, bufferedList);
        }


    }
}
